package com.eobard.obfuscate.optional;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.eobard.config.obfuscate.ObfuscatorConfig;
import com.eobard.enums.RunningEnum;
import com.eobard.obfuscate.IObfuscator;
import com.eobard.params.RenameResourceParams;
import com.eobard.utils.*;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.expr.FieldAccessExpr;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.io.File;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * @Author: Eobard Gu
 * @Date: 2025-08-08
 * @Description: 重命名安卓项目xml资源文件核心混淆类
 * <p>
 * 修改资源文件夹的规则:
 * <ul>
 *     <li>
 *         文件夹中属于/drawable、/drawable-xxx之类的
 *         <p>
 *             对应其它xml的引用为:@/drawable/旧文件名;如果需要重命名需要改为:@/drawable/新文
 *             对应java的引用为: R.drawable.旧文件名;如果需要重命名需要改为:R.drawable.新文
 *         </p>
 *     </li>
 *      <li>
 *          文件夹中属于/layout
 *           <p>
 *              对应其它xml的引用为:@layout/旧文件名;如果需要重命名需要改为:@layout/新文件名 <b
 *              对应java的引用为: R.layout.旧文件名;如果需要重命名需要改为:R.layout.新文件名
 *           </p>
 *      </li>
 *      <li>
 *          文件夹属于/raw、/raw-xxx之类的
 *          <p>
 *              对应其它xml的引用为:暂无引用 <br>
 *              对应java的引用为: R.raw.旧文件名;如果需要重命名需要改为:R.raw.新文件名
 *          </p>
 *      </li>
 *      <li>
 *          文件夹属于/values、/values-xx之类的(随意重命名文件名称,不用变name值即可)
 *          <p>
 *              对应其它xml的引用为:@string/里面的name值 <br>
 *              对应java的引用为: R.string.里面的name值
 *          </p>
 *      </li>
 *      <li>
 *           文件夹属于/xml
 *           <p>
 *               对应其它xml的引用为:@xml/旧文件名;如果需要重命名需要改为:@xml/新文件名 <br>
 *               对应java的引用为              暂无引用
 *           </p>
 *      </li>
 * </ul>
 * </p>
 */
@Slf4j
@Component
public class RenameResourceObfuscator implements IObfuscator {
    //资源文件主配置文件
    private static final String ANDROID_MANIFEST = "AndroidManifest.xml";

    //代码根路径
    private static String sourceRoot;

    //资源文件的根路径
    private static String resourceRoot;

    //需要重命名的资源文件夹名称
    private static List<String> dirs = null;

    //对应dirs资源文件夹里文件的公共前缀
    // eg: drawable/下文件都以com_pask_living开头,这里就可以填写com_pask_living
    //     drawable-xhdpi/下文件都是以com_pxt_living开头,这里就可以填写com_pxt_living
    private static List<String> resourceOldPrefix = null;

    //对应dirs资源文件夹里文件的目标公共前缀
    //  eg: drawable/下文件都以com_pask_living开头,你想要变为com_gxp_living,这里就可以填写com_gxp_living
    //      drawable-xhdpi/下文件都是以com_pxt_living开头,你想要变为com_eobard_living,这里就可以填写com_eobard_living
    private static List<String> resourceNewPrefix = null;

    //所有java文件map
    private static Map<String, CompilationUnit> fileMap = new HashMap<>();

    //所有xml资源文件map
    private static Map<String, File> allResourcesMap = new HashMap<>();

    //用于修改java或xml引用的map,k为文件名,v为getResourceReferenceValue中获取的值
    private static Map<String, String> referenceMap = new HashMap<>();


    //数据初始化和检查代码
    @Override
    public void initialize(ObfuscatorConfig config) {
        if (config.getRunningStatus() != RunningEnum.ANDROID_PROjECT)
            throw new RuntimeException("请先设置ObfuscatorConfig中runningStatus字段为安卓项目");
        CollectionUtils.clear(dirs, resourceOldPrefix, resourceNewPrefix);
        CollectionUtils.clear(fileMap, allResourcesMap, referenceMap);

        RenameResourceParams params = config.getRenameResourceParams();
        sourceRoot = config.getSourceRoot();
        resourceRoot = config.getResourceRoot();

        dirs = params.getDirs();

        if (StrUtil.isBlank(sourceRoot) || !sourceRoot.trim().endsWith("/java"))
            throw new RuntimeException("安卓的Java资源路径应该以/java文件夹结尾");

        ExceptionUtils.checkNonNull(dirs, "请先选择所选资源文件夹！");

        resourceOldPrefix = params.getResourceOldPrefix();
        resourceNewPrefix = params.getResourceNewPrefix();

        //加载所有java文件
        FileUtils.loopFiles(fileMap, sourceRoot);
        if (CollectionUtil.isEmpty(fileMap)) throw new RuntimeException("fileMap为空,请检查sourceRoot的路径");

        //加载resourceRoot路径下所有重命名xml资源文件
        ExceptionUtils.checkNonNull(resourceRoot, "请在ObfuscatorConfig中配置resourceRoot资源文件路径");

        FileUtil.loopFiles(resourceRoot, FileUtils::isResourceFile).forEach(p -> allResourcesMap.put(p.toString(), p));
        ExceptionUtils.checkNonNull(allResourcesMap, "allResourcesMap为空，请检查resourceRoot路径是否正确");


        //检查dirs文件夹
        FileUtils.checkDirectoryExist(dirs, resourceRoot);

        ExceptionUtils.checkNonNull(resourceOldPrefix, "资源文件的旧前缀不能为空");
        ExceptionUtils.checkNonNull(resourceNewPrefix, "重命名后的资源文件目标前缀不能为空");

        //xml资源文件前缀检查
        if (resourceNewPrefix.size() > resourceOldPrefix.size())
            throw new RuntimeException("resourceNewPrefix的长度不能超过resourceOldPrefix");
        if (resourceOldPrefix.size() > resourceNewPrefix.size() && resourceNewPrefix.size() != 1)
            throw new RuntimeException("多个resourceOldPrefix只能重命名一个resourceNewPrefix");
    }

    //核心混淆代码
    @Override
    public void obfuscate(ObfuscatorConfig config) {
        //1. 获取所有资源文件中属于dirs文件夹中的xml资源文件
        Map<String, File> targetResourcesMap = allResourcesMap.entrySet()
                .stream()
                .filter(entry -> {
                    //判断resourceRoot是不是以/结尾
                    int length = resourceRoot.endsWith(File.separator) ? resourceRoot.length() : resourceRoot.length() + 1;
                    //获取文件的父文件夹
                    String parentDir = Paths.get(entry.getKey()).getParent().toString().substring(length);
                    //todo:将所有的allResourcesMap都填充referenceMap数据,这里可在过滤后再填充,可以优化referenceMap的空间
                    referenceMap.put(entry.getKey(), ObfuscatorUtils.getResourceReferenceValue(parentDir));
                    return dirs.stream().anyMatch(parentDir::equalsIgnoreCase);
                })
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));


        ExceptionUtils.checkNonNull(targetResourcesMap, "targetResourcesMap为空，请检查dirs是否包含在sourceRoot里面");

        //2.默认添加AndroidManifest.xml文件
        String androidManifest = sourceRoot.replace("java", "") + ANDROID_MANIFEST;
        if (FileUtil.exist(androidManifest) && !allResourcesMap.containsKey(androidManifest)) {
            allResourcesMap.put(androidManifest, new File(androidManifest));
        } else {
            log.error("=========================⬇️============================");
            log.error(ANDROID_MANIFEST + "文件未找到,将跳过修改此文件,请手动修改文件");
            log.error("=========================⬆️============================");
        }

        //3.开始重命名xml资源文件
        targetResourcesMap.entrySet()
                .stream()
                .map(Map.Entry::getValue)
                .forEach(f -> {
                    //原始的旧名称
                    String oldName = f.getName();
                    // 情况1处理
                    if (resourceOldPrefix.size() == resourceNewPrefix.size()) {
                        IntStream.range(0, resourceOldPrefix.size())
                                .filter(i -> oldName.startsWith(resourceOldPrefix.get(i)))      //如果当前文件的名称是以resourceOldPrefix开头的
                                .findFirst()                                                    //那么我就找到它然后处理
                                .ifPresent(i -> {
                                    //一一对应resourceOldPrefix和resourceNewPrefix的每一个值
                                    String oldP = resourceOldPrefix.get(i);
                                    String newP = resourceNewPrefix.get(i);
                                    //处理
                                    this.process(f, oldName.replaceFirst(oldP, newP));
                                });
                    }
                    // 情况2处理
                    else if (resourceNewPrefix.size() == 1 && resourceOldPrefix.size() > 1) {
                        String targetPrefix = resourceNewPrefix.get(0);
                        resourceOldPrefix.stream()
                                .filter(oldName::startsWith)        //如果当前文件的名称是以resourceOldPrefix开头的
                                .findFirst()                        //那么我就去处理它
                                .ifPresent(oldP -> this.process(f, oldName.replaceFirst(oldP, targetPrefix)));
                    }
                });
        //4.保存修改后的文件
        FileUtils.saveObfuscatedFiles(fileMap);

    }

    //是否启动这个功能
    @Override
    public boolean supports(ObfuscatorConfig config) {
        return ObjectUtil.isNotEmpty(config.getRenameResourceParams());
    }

    //重命名文件本身、java引用、xml引用逻辑
    private void process(File f, String newPrefix) {
        //1.重命名文件本身
        File rename = FileUtil.rename(f, newPrefix, true);
        //2.更新allResourcesMap数据
        allResourcesMap.remove(f.toString());
        allResourcesMap.put(rename.toString(), rename);
        //3.重命名java引用
        updateJavaFiledReference(f, newPrefix);
        //4.修改其它资源文件的引用(TODO:这个方法可以优化,这里面的逻辑就算没有修改xml也会重新写出文件,代码里面的判断是否修改文件逻辑失效了)
        updateXmlReference(f, newPrefix);
    }

    //更改java代码中资源字段引用的地方
    private static void updateJavaFiledReference(File f, String newFieldName) {
        //需要处理文件的最后扩展名
        String oldFiledName = f.getName();
        int oIndex = oldFiledName.lastIndexOf(".");
        int nIndex = newFieldName.lastIndexOf(".");
        if (oIndex != -1) {
            oldFiledName = oldFiledName.substring(0, oIndex);
        }
        if (nIndex != -1) {
            newFieldName = newFieldName.substring(0, nIndex);
        }
//        System.err.printf("重命名资源文件:\n\t%s\t %s \t -> %s%n", f.getParent().substring(resourceRoot.length()), oldFiledName, newFieldName);

        //获取java代码中的引用位置
        String finalOldFiledName = oldFiledName;
        String finalNewFieldName = newFieldName;
        fileMap.entrySet()
                .stream()
                .map(Map.Entry::getValue)
                .forEach(cu ->
                                //获取对字段变量、字段常量访问的表达式
                                cu.findAll(FieldAccessExpr.class).forEach(expr -> {
                                    //获取referenceMap中的值,用于访问java中的引用
                                    String referenceValue = referenceMap.get(f.toString());
                                    String javaReference = "R." + referenceValue + ".";
                                    //获取字段出现的位置
                                    if (expr.toString().startsWith(javaReference)) {
                                        //只保留最外层FieldAccessExpr
                                        if (expr.isTopLevel()) {
                                            String fieldName = expr.getName().toString();
                                            //如果名称对应则重新设置字段名称
                                            if (fieldName.equals(finalOldFiledName)) {
                                                //设置原本的作用域
                                                expr.setScope(expr.getScope());
                                                //设置新的字段名称
                                                expr.setName(finalNewFieldName);
//                                        System.err.println("\tjava引用修改:\n\t\t" + ObfuscatorUtils.getClassName(cu, true) + "\t" + fieldName + "\t->\t" + finalNewFieldName);
                                            }
                                        }
                                    }
                                })
                );
    }

    //更改xml中引用的地方
    private static void updateXmlReference(File f, String newFieldName) {
        String oldFiledName = f.getName();
        int oIndex = oldFiledName.lastIndexOf(".");
        int nIndex = newFieldName.lastIndexOf(".");
        if (oIndex != -1) {
            oldFiledName = oldFiledName.substring(0, oIndex);
        }
        if (nIndex != -1) {
            newFieldName = newFieldName.substring(0, nIndex);
        }

        final String finalOldName = oldFiledName;
        final String finalNewName = newFieldName;

        //获取referenceMap中的值,用于访问xml的引用
        String referenceValue = referenceMap.get(f.toString());
        String oldXmlReference = "@" + referenceValue + File.separator + finalOldName;
        String newXmlReference = "@" + referenceValue + File.separator + finalNewName;

        //遍历整个资源文件,将引用旧xml文件的地方更改为新的名称
        allResourcesMap.entrySet()
                .stream()
                .filter(item -> item.getKey().endsWith(".xml"))       //过滤掉非xml文件引用的
                .forEach(item -> {
                    XmlUtils.replaceAttributeValues(String.valueOf(item.getValue()), oldXmlReference, newXmlReference);
                });
    }

}
